package com.luych.toolbox.data.service.service;

import com.luych.toolbox.common.enums.DataTransporterPlanStatus;
import com.luych.toolbox.common.feign.intf.base.service.UserFeign;
import com.luych.toolbox.common.feign.param.data.service.DataTransporterPlanParam;
import com.luych.toolbox.common.feign.view.data.service.DataSourceView;
import com.luych.toolbox.common.feign.view.data.service.DataTransporterPlanView;
import com.luych.toolbox.common.page.PageParam;
import com.luych.toolbox.common.page.PageResult;
import com.luych.toolbox.common.service.base.BaseConvert;
import com.luych.toolbox.common.service.base.BaseService;
import com.luych.toolbox.common.service.exception.HttpException;
import com.luych.toolbox.common.service.page.SpringPageParamAdapter;
import com.luych.toolbox.common.service.page.SpringPageResultAdapter;
import com.luych.toolbox.data.service.constant.Constant;
import com.luych.toolbox.data.service.convert.DataTransporterPlanEntityToView;
import com.luych.toolbox.data.service.convert.DataTransporterPlanParamToEntity;
import com.luych.toolbox.data.service.dao.DataSourceDao;
import com.luych.toolbox.data.service.dao.DataTransporterHistoryDao;
import com.luych.toolbox.data.service.dao.DataTransporterPlanDao;
import com.luych.toolbox.data.service.entity.DataSource;
import com.luych.toolbox.data.service.entity.DataTransporterPlan;
import com.luych.toolbox.data.service.job.DataTransporterPlanExecuteJob;
import org.quartz.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

@Service
public class DataTransporterPlanService
        extends BaseService<DataTransporterPlanView, DataTransporterPlanParam, DataTransporterPlan>
        implements ApplicationListener<ApplicationReadyEvent> {
    private static final Logger LOGGER = LoggerFactory.getLogger(DataTransporterPlanService.class);

    private static final BaseConvert<DataTransporterPlanParam, DataTransporterPlan> dataTransporterPlanParamToEntity = new DataTransporterPlanParamToEntity();
    private static final BaseConvert<DataTransporterPlan, DataTransporterPlanView> dataTransporterPlanEntityToView = new DataTransporterPlanEntityToView();
    private static final BaseConvert<DataSource, DataSourceView> dataSourceEntityToView = DataSourceView::new;

    private final Boolean rescheduleWhenStart;

    private final Scheduler scheduler;

    private final DataTransporterPlanDao dataTransporterPlanDao;
    private final DataTransporterHistoryDao dataTransporterHistoryDao;
    private final DataSourceDao dataSourceDao;

    private final UserFeign userFeign;

    @Autowired
    public DataTransporterPlanService(@Value("${spring.quartz.rescheduleWhenStart:false}") Boolean rescheduleWhenStart,
                                      Scheduler scheduler,
                                      DataTransporterPlanDao dataTransporterPlanDao,
                                      DataTransporterHistoryDao dataTransporterHistoryDao,
                                      DataSourceDao dataSourceDao,
                                      UserFeign userFeign) {
        super(dataTransporterPlanDao, dataTransporterPlanParamToEntity, dataTransporterPlanEntityToView);
        this.rescheduleWhenStart = rescheduleWhenStart;
        this.scheduler = scheduler;
        this.dataTransporterPlanDao = dataTransporterPlanDao;
        this.dataTransporterHistoryDao = dataTransporterHistoryDao;
        this.dataSourceDao = dataSourceDao;
        this.userFeign = userFeign;
    }

    @Override
    protected DataTransporterPlanView fill(DataTransporterPlanView view) {
        Optional<DataTransporterPlan> dataTransporterPlanOpt = dataTransporterPlanDao.findById(view.getId());
        dataTransporterPlanOpt.map(DataTransporterPlan::getDataSourceFromId).flatMap(dataSourceDao::findById).map(dataSourceEntityToView).ifPresent(view::setDataSourceFrom);
        dataTransporterPlanOpt.map(DataTransporterPlan::getDataSourceToId).flatMap(dataSourceDao::findById).map(dataSourceEntityToView).ifPresent(view::setDataSourceTo);
        dataTransporterPlanOpt.map(DataTransporterPlan::getOwnerId).map(userFeign::get).ifPresent(view::setOwner);
        return super.fill(view);
    }

    private JobKey getJobKey(String id) {
        return new JobKey(id, DataTransporterPlanExecuteJob.class.getName());
    }

    private JobDetail getJobDetail(String id) {
        Map<String, Object> jobParam = new HashMap<String, Object>() {{
            this.put("id", id);
        }};
        return JobBuilder.newJob(DataTransporterPlanExecuteJob.class)
                .withIdentity(this.getJobKey(id))
                .usingJobData(new JobDataMap(jobParam))
                .build();
    }

    private Trigger getJobTrigger(String id) {
        DataTransporterPlan dataTransporterPlan = dataTransporterPlanDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_COMPARE_PLAN_NOT_FOUND));
        return TriggerBuilder.newTrigger()
                .withIdentity(id, DataTransporterPlanExecuteJob.class.getName())
                .withSchedule(CronScheduleBuilder.cronSchedule(dataTransporterPlan.getCron()))
                .build();
    }

    @Override
    public DataTransporterPlanView save(DataTransporterPlanParam param) {
        // check corn
        if (!CronExpression.isValidExpression(param.getCron())) {
            throw new HttpException(HttpStatus.BAD_REQUEST, Constant.DATA_COMPARE_PLAN_SAVE_CRON_ERROR);
        }
        // check
        DataSource dataSourceOne = dataSourceDao.findById(param.getDataSourceFromId()).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_SOURCE_NOT_FOUND));
        DataSource dataSourceTwo = dataSourceDao.findById(param.getDataSourceToId()).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_SOURCE_NOT_FOUND));

        // save
        DataTransporterPlan dataTransporterPlanParam = dataTransporterPlanParamToEntity.apply(param);
        dataTransporterPlanParam.setStatus(DataTransporterPlanStatus.LOADING);
        DataTransporterPlan dataTransporterPlanResult = dataTransporterPlanDao.save(dataTransporterPlanParam);
        return this.fill(dataTransporterPlanEntityToView.apply(dataTransporterPlanResult));
    }

    private void setStatus(String id, DataTransporterPlanStatus status) {
        DataTransporterPlan dataTransporterPlan = dataTransporterPlanDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_COMPARE_PLAN_NOT_FOUND));
        dataTransporterPlan.setStatus(status);
        dataTransporterPlanDao.save(dataTransporterPlan);
    }

    @Override
    public DataTransporterPlanView create(DataTransporterPlanParam param) {
        Assert.notNull(param.getDataSourceFromId(), "Param dataSourceFromId can't be empty");
        Assert.notNull(param.getDataSourceToId(), "Param dataSourceToId can't be empty");
        Assert.notNull(param.getName(), "Param name can't be empty");
        Assert.notNull(param.getCron(), "Param cron can't be empty");
        Assert.notNull(param.getOwnerId(), "Param ownerId can't be empty");

        DataTransporterPlanView view = this.save(param);

        try {
            scheduler.scheduleJob(this.getJobDetail(view.getId()), this.getJobTrigger(view.getId()));
            view.setStatus(DataTransporterPlanStatus.ENABLE);
            this.setStatus(view.getId(), DataTransporterPlanStatus.ENABLE);
        } catch (SchedulerException e) {
            view.setStatus(DataTransporterPlanStatus.EXCEPTION);
            this.setStatus(view.getId(), DataTransporterPlanStatus.EXCEPTION);
            throw new IllegalStateException(e);
        }

        return view;
    }

    @Override
    public DataTransporterPlanView modify(DataTransporterPlanParam param) {
        Assert.notNull(param.getId(), "Param id can't be empty");
        Assert.notNull(param.getDataSourceFromId(), "Param dataSourceFromId can't be empty");
        Assert.notNull(param.getDataSourceToId(), "Param dataSourceToId can't be empty");
        Assert.notNull(param.getName(), "Param name can't be empty");
        Assert.notNull(param.getCron(), "Param cron can't be empty");
        Assert.notNull(param.getOwnerId(), "Param ownerId can't be empty");

        DataTransporterPlanView view = this.save(param);

        try {
            scheduler.deleteJob(this.getJobKey(view.getId()));
            scheduler.scheduleJob(this.getJobDetail(view.getId()), this.getJobTrigger(view.getId()));
            view.setStatus(DataTransporterPlanStatus.ENABLE);
            this.setStatus(view.getId(), DataTransporterPlanStatus.ENABLE);
        } catch (SchedulerException e) {
            view.setStatus(DataTransporterPlanStatus.EXCEPTION);
            this.setStatus(view.getId(), DataTransporterPlanStatus.EXCEPTION);
            throw new IllegalStateException(e);
        }

        return view;
    }

    @Override
    public void remove(String id) {
        int used = 0;
        used += dataTransporterHistoryDao.countByDataTransporterPlanId(id);
        if (used > 0) {
            throw new HttpException(HttpStatus.FORBIDDEN, Constant.DATA_COMPARE_PLAN_DELETE_REJECT_USED);
        }

        try {
            scheduler.deleteJob(this.getJobKey(id));
        } catch (SchedulerException e) {
            throw new IllegalStateException(e);
        }

        dataTransporterPlanDao.deleteById(id);
    }

    public void setEnable(String id) {
        DataTransporterPlan dataTransporterPlan = dataTransporterPlanDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_COMPARE_PLAN_NOT_FOUND));

        if (!DataTransporterPlanStatus.DISABLE.equals(dataTransporterPlan.getStatus())) {
            throw new HttpException(HttpStatus.FORBIDDEN, Constant.DATA_COMPARE_PLAN_ENABLE_STATUS_ERROR);
        }

        try {
            scheduler.resumeJob(this.getJobKey(id));
        } catch (SchedulerException e) {
            throw new IllegalStateException(e);
        }

        dataTransporterPlan.setStatus(DataTransporterPlanStatus.ENABLE);
        dataTransporterPlanDao.save(dataTransporterPlan);
    }

    public void setDisable(String id) {
        DataTransporterPlan dataTransporterPlan = dataTransporterPlanDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_COMPARE_PLAN_NOT_FOUND));

        if (!DataTransporterPlanStatus.ENABLE.equals(dataTransporterPlan.getStatus())) {
            throw new HttpException(HttpStatus.FORBIDDEN, Constant.DATA_COMPARE_PLAN_DISABLE_STATUS_ERROR);
        }

        try {
            scheduler.pauseJob(this.getJobKey(id));
        } catch (SchedulerException e) {
            throw new IllegalStateException(e);
        }

        dataTransporterPlan.setStatus(DataTransporterPlanStatus.DISABLE);
        dataTransporterPlanDao.save(dataTransporterPlan);
    }

    public PageResult<DataTransporterPlanView> pageByKeyword(PageParam pageParam, String keyword) {
        Page<DataTransporterPlan> page;
        if (StringUtils.isEmpty(keyword)) {
            page = dataTransporterPlanDao.findAll(new SpringPageParamAdapter(pageParam));
        } else {
            page = dataTransporterPlanDao.findByNameLike("%" + keyword + "%", new SpringPageParamAdapter(pageParam));
        }
        PageResult<DataTransporterPlan> pageResult = new SpringPageResultAdapter<>(page);
        return pageResult.convert(dataTransporterPlanEntityToView).convert(this::fill);
    }

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        if (Boolean.TRUE.equals(rescheduleWhenStart)) {
            List<DataTransporterPlan> dataTransporterPlans = dataTransporterPlanDao.findByStatusNot(DataTransporterPlanStatus.DISABLE);
            for (DataTransporterPlan dataTransporterPlan : dataTransporterPlans) {
                try {
                    scheduler.deleteJob(this.getJobKey(dataTransporterPlan.getId()));
                    scheduler.scheduleJob(this.getJobDetail(dataTransporterPlan.getId()), this.getJobTrigger(dataTransporterPlan.getId()));
                    this.setStatus(dataTransporterPlan.getId(), DataTransporterPlanStatus.ENABLE);
                } catch (SchedulerException e) {
                    this.setStatus(dataTransporterPlan.getId(), DataTransporterPlanStatus.EXCEPTION);
                    throw new IllegalStateException(e);
                }
            }
        }
    }
}
